import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from typing import List, Union, Tuple


# ==================== 配置参数与字体设置 ====================
# 获取当前脚本目录
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
# 强制使用的字体路径
FONT_ABSOLUTE_PATH = os.path.join(
    CURRENT_DIR, "ziti", "NotoSansCJKsc", "NotoSansCJKsc-Regular.otf"
)


def get_custom_font(size: int = 12) -> FontProperties:
    """获取指定路径的字体，确保中文正常显示"""
    if not os.path.exists(FONT_ABSOLUTE_PATH):
        raise FileNotFoundError(
            f"字体文件不存在，请检查路径: {FONT_ABSOLUTE_PATH}\n"
            f"当前脚本目录: {CURRENT_DIR}"
        )
    return FontProperties(fname=FONT_ABSOLUTE_PATH, size=size)


# ==================== 情感满意度变换 ====================
def transform_satisfaction(satisfaction: int) -> float:
    """
    将离散情感满意度(-1, 0, 1)转换为相关性分数
    转换规则：负面→0，中性→1，正面→2（可根据需求调整权重）
    """
    score_map = {-1: 0.0, 0: 1.0, 1: 2.0}
    return score_map.get(satisfaction, 0.0)  # 异常值默认0


# ==================== NDCG计算核心函数 ====================
def dcg_at_k(relevance_scores: Union[List[float], np.ndarray], k: int) -> float:
    """计算前k个推荐结果的折扣累积增益(DCG)"""
    relevance = np.asarray(relevance_scores)[:k]  # 截取前k个结果
    if relevance.size == 0:
        return 0.0
    # 折扣因子：log2(i+2)，i从0开始（第1个结果无折扣）
    discounts = np.log2(np.arange(2, relevance.size + 2))
    return np.sum(relevance / discounts)


def idcg_at_k(relevance_scores: Union[List[float], np.ndarray], k: int) -> float:
    """计算理想折扣累积增益(IDCG)，即最优排序的DCG"""
    sorted_relevance = np.sort(relevance_scores)[::-1]  # 按相关性降序排列
    return dcg_at_k(sorted_relevance, k)


def ndcg_at_k(relevance_scores: Union[List[float], np.ndarray], k: int) -> float:
    """计算归一化折扣累积增益(NDCG) = DCG / IDCG"""
    dcg = dcg_at_k(relevance_scores, k)
    if dcg == 0:
        return 0.0
    ideal_dcg = idcg_at_k(relevance_scores, k)
    return dcg / ideal_dcg if ideal_dcg != 0 else 0.0


def average_ndcg(scores_list: List[List[float]], k: int) -> Tuple[float, List[float]]:
    """计算多个用户的平均NDCG@k"""
    per_user_ndcg = [ndcg_at_k(scores, k) for scores in scores_list]
    return np.mean(per_user_ndcg), per_user_ndcg


# ==================== 数据处理函数 ====================
def load_and_merge_data(
    satisfaction_path: str, recommendation_path: str
) -> pd.DataFrame:
    """加载情感满意度数据和推荐列表，按用户-电影ID合并"""
    # 加载情感满意度数据
    try:
        satisfaction_df = pd.read_csv(
            satisfaction_path,
            usecols=["user_id", "movie_id", "satisfaction"],
            dtype={"user_id": str, "movie_id": str, "satisfaction": int}
        )
        print(f"加载情感满意度数据：{len(satisfaction_df)} 条记录")
    except Exception as e:
        raise RuntimeError(f"情感满意度数据加载失败：{str(e)}")

    # 加载推荐列表（含推荐顺序）
    try:
        recommendation_df = pd.read_csv(
            recommendation_path,
            usecols=["user_id", "movie_id", "rank"],
            dtype={"user_id": str, "movie_id": str, "rank": int}
        )
        print(f"加载推荐列表数据：{len(recommendation_df)} 条记录")
    except Exception as e:
        raise RuntimeError(f"推荐列表数据加载失败：{str(e)}")

    # 合并数据（关联用户-电影对的情感满意度）
    merged_df = pd.merge(
        recommendation_df,
        satisfaction_df,
        on=["user_id", "movie_id"],
        how="left"
    )

    # 处理缺失的满意度（默认中性0）
    merged_df["satisfaction"] = merged_df["satisfaction"].fillna(0).astype(int)
    missing_ratio = merged_df["satisfaction"].isna().mean() * 100
    print(f"合并后数据：{len(merged_df)} 条记录，缺失满意度比例：{missing_ratio:.2f}%")

    return merged_df


def prepare_relevance_scores(merged_df: pd.DataFrame) -> Tuple[List[List[float]], List[str]]:
    """按用户分组，生成每个用户的推荐列表相关性分数（按推荐顺序）"""
    # 按用户ID和推荐排名排序（确保顺序正确）
    merged_df = merged_df.sort_values(["user_id", "rank"])

    # 按用户分组提取相关性分数
    user_groups = merged_df.groupby("user_id")
    relevance_scores_list = []
    user_ids = []

    for user_id, group in user_groups:
        user_ids.append(user_id)
        # 转换情感满意度为相关性分数
        relevance_scores = group["satisfaction"].apply(transform_satisfaction).tolist()
        relevance_scores_list.append(relevance_scores)

    print(f"完成相关性分数提取：共 {len(user_ids)} 个用户")
    return relevance_scores_list, user_ids


# ==================== 主函数 ====================
def main():
    # 配置文件路径
    SATISFACTION_PATH = "results/svd_detailed_results copy.csv"
    RECOMMENDATION_PATH = "recommendation_results/top50_users_svd_detailed.csv"
    K = 10  # 计算NDCG@3

    # 创建结果目录
    os.makedirs("results", exist_ok=True)

    try:
        # 1. 加载并合并数据
        merged_df = load_and_merge_data(SATISFACTION_PATH, RECOMMENDATION_PATH)

        # 2. 生成相关性分数
        relevance_scores_list, user_ids = prepare_relevance_scores(merged_df)

        # 3. 计算NDCG@3
        avg_ndcg, per_user_ndcg = average_ndcg(relevance_scores_list, K)

        # 4. 输出统计结果
        print("\n" + "="*30)
        print(f"===== NDCG@{K} 计算结果 =====")
        print(f"用户数量：{len(per_user_ndcg)}")
        print(f"平均 NDCG@{K}：{avg_ndcg:.4f}")
        print(f"最小 NDCG@{K}：{min(per_user_ndcg):.4f}")
        print(f"最大 NDCG@{K}：{max(per_user_ndcg):.4f}")
        print(f"中位数 NDCG@{K}：{np.median(per_user_ndcg):.4f}")
        print("="*30 + "\n")

        # 5. 绘制并保存直方图（强制使用指定字体）
        plt.figure(figsize=(10, 6))
        
        # 设置字体
        title_font = get_custom_font(size=14)
        label_font = get_custom_font(size=12)
        tick_font = get_custom_font(size=10)
        legend_font = get_custom_font(size=10)

        # 绘制直方图
        plt.hist(
            per_user_ndcg,
            bins=10,
            alpha=0.7,
            color="#4CAF50"
        )
        plt.axvline(
            avg_ndcg,
            color="red",
            linestyle="--",
            linewidth=2,
            label=f"平均值: {avg_ndcg:.4f}"
        )

        # 设置图表属性
        plt.title(f"用户NDCG@{K}分布", fontproperties=title_font)
        plt.xlabel(f"NDCG@{K} 值", fontproperties=label_font)
        plt.ylabel("用户数量", fontproperties=label_font)
        plt.xticks(fontproperties=tick_font)
        plt.yticks(fontproperties=tick_font)
        plt.legend(prop=legend_font)
        plt.grid(axis="y", alpha=0.3)
        plt.tight_layout()

        # 保存图片
        img_path = f"ndcg/svd_ndcg_satisfaction@{K}_distribution.png"
        plt.savefig(img_path, dpi=300)
        print(f"直方图已保存至：{img_path}")

        # 6. 保存详细结果
        result_df = pd.DataFrame({
            "user_id": user_ids,
            f"ndcg@{K}": per_user_ndcg
        })
        result_path = f"ndcg/svd_user_ndcg_satisfaction@{K}.csv"
        result_df.to_csv(result_path, index=False)
        print(f"详细结果已保存至：{result_path}")

    except Exception as e:
        print(f"执行失败：{str(e)}")


if __name__ == "__main__":
    main()